package cn.tedu.mall.seckill.timer.job;

import cn.tedu.mall.common.config.PrefixConfiguration;
import cn.tedu.mall.pojo.seckill.model.SeckillSku;
import cn.tedu.mall.pojo.seckill.model.SeckillSpu;
import cn.tedu.mall.seckill.mapper.SeckillSkuMapper;
import cn.tedu.mall.seckill.mapper.SeckillSpuMapper;
import cn.tedu.mall.seckill.utils.SeckillCacheUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.math.RandomUtils;
import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import java.time.LocalDateTime;
import java.util.List;
import java.util.concurrent.TimeUnit;

@Slf4j
public class SeckillInitialJob implements Job {

    // 查询spu相关信息的mapper
    @Autowired
    private SeckillSpuMapper seckillSpuMapper;
    // 查询sku相关信息的mapper
    @Autowired
    private SeckillSkuMapper seckillSkuMapper;
    // 操作Redis的对象
    @Autowired
    private RedisTemplate redisTemplate;

    /*
       RedisTemplate对象再保存数据到Redis时,会将数据进行序列化后保存
       这样做的好处是对java对象获取对象格式的数据读写效率高,占用空间小
       缺点是不能再Redis内部对这个数据进行修改
       现在我们要保存的数据是秒杀sku库存数,这个数据如果用RedisTemplate保存
       就会再高并发情况下容易出现线程安全问题,导致商品库存的"超卖"
       解决办法就是需要一个能够在Redis内部直接修改数据的方式,避免线程安全问题,防止"超卖"
       使用Spring Data Redis框架提供的StringRedisTemplate类型对象,它可以在Redis内部修改值
       StringRedisTemplate只能向Redis中保存字符串,如果是数值类型的内容,支持数据的增减
       没有序列化过程,这样java代码就不需要取出数据在进行操作了
       最后因为Redis内部操作数据的线程是单线程的特征,也能从Redis层面防止超卖
       如果Redis有多台服务器,要想保证数据同步,就需要redission分布式锁
    */
    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Override
    public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
        // 当前方法的目标是将sku库存数和随机码预热到redis中
        // 本方法运行时机是秒杀开始前5分钟,所以我们需要创建一个5分钟的时间,用于查询秒杀的商品列表
        LocalDateTime time=LocalDateTime.now().plusMinutes(5);
        // 查询5分钟后所有进行秒杀的spu商品列表
        List<SeckillSpu> seckillSpus=seckillSpuMapper.findSeckillSpusByTime(time);
        // 遍历当前批次所有参加秒杀的spu列表
        for(SeckillSpu spu : seckillSpus){
            // 我们是要将sku的库存数预热到redis,所以要根据当前spuId查询sku列表
            List<SeckillSku> seckillSkus=seckillSkuMapper
                                    .findSeckillSkusBySpuId(spu.getSpuId());
            // 遍历seckillSkus集合,从中获取sku对象,以向Redis中保存它的库存数
            for(SeckillSku sku: seckillSkus){
                log.info("开始将{}号的sku库存数预热到redis",sku.getSkuId());
                // 要操作Redis,需要先确定Redis的Key,一般都是定义好的常量
                // SeckillCacheUtils.getStockKey是返回字符串常量的方法
                // 这个方法的参数sku.getSkuId(),会追加到字符串常量的最后
                // skuStockKey最后的值可能是:mall:seckill:sku:stock:1
                String skuStockKey= SeckillCacheUtils.getStockKey(sku.getSkuId());
                // 检查Redis中是否已经存在这个key
                if(redisTemplate.hasKey(skuStockKey)){
                    // 如果这个Key已经存在了,证明之前缓存过了,直接跳过
                    log.info("{}号sku的库存数,已经缓存过了",sku.getSkuId());
                }else{
                    // 如果这个Key不存在,就要将库存数保存到Redis中
                    stringRedisTemplate.boundValueOps(skuStockKey).set(
                            sku.getSeckillStock()+"",
                            // 保存时间: 秒杀时间 + 提前的5分钟+ 防雪崩随机数30秒
                            //1000*60*60*2 + 1000*60*5 + RandomUtils.nextInt(30000),
                            1000*60*5+RandomUtils.nextInt(10000),
                            TimeUnit.MILLISECONDS);
                    log.info("{}号sku的库存数成功预热到缓存!",sku.getSkuId());
                }
            }
            // 上面是内层循环结束,但是当前仍然在外层循环结构中
            // 遍历的是spu对象,我们生成的随机码和spu关联
            // 随机码就是个随机数,随机范围自定即可
            // 将这个随机码保存到Redis指定的key中
            // 确定随机码的key  mall:seckill:spu:url:rand:code:2
            String randCodeKey=SeckillCacheUtils.getRandCodeKey(spu.getSpuId());
            // 判断当前Key是否在Redis中
            if(redisTemplate.hasKey(randCodeKey)){
                // 如果随机码已经存在了,就需要任何操作了
                // 但是为了今后我们自己测试方便,也要输出到控制台
                int randCode= (int) redisTemplate.boundValueOps(randCodeKey).get();
                log.info("{}号spu商品的随机码已经缓存过了,值为:{}",
                                               spu.getSpuId(),randCode);
            }else{
                // 如果不存在key,就要生成随机码保存再Redis中
                // 我们设计生成的随机码范围100000-999999
                int randCode=RandomUtils.nextInt(900000)+100000;
                redisTemplate.boundValueOps(randCodeKey).set(
                        randCode,
                        1000*60*5+RandomUtils.nextInt(10000),
                        TimeUnit.MILLISECONDS);
                log.info("{}号spu商品的随机码缓存成功!,值为:{}",
                        spu.getSpuId(),randCode);
            }
        }
    }
}
